Skip to content

feat(django): Support span streaming#6248

Merged
alexander-alderman-webb merged 23 commits into
masterfrom
webb/django/span-first
May 20, 2026
Merged

feat(django): Support span streaming#6248
alexander-alderman-webb merged 23 commits into
masterfrom
webb/django/span-first

Conversation

@alexander-alderman-webb
Copy link
Copy Markdown
Contributor

@alexander-alderman-webb alexander-alderman-webb commented May 11, 2026

Description

There are 3 event processors in the integration. None of these need to be ported:

  • process_django_templates() exits early if the event is not an exception
  • asgi_request_event_processor() adds request info (out of scope for span first).
  • wsgi_request_event_processor() adds request info (out of scope for span first).

In the streaming path, use

  • db.namespace instead of db.name
  • db.system.name instead of db.system
  • db.operation.name instead of db.operation
  • middleware.name instead of django.middleware_name
  • code.function.name instead of signal
  • code.filepath instead of code.file.path
  • code.line.number instead of code.lineno

Dropped attributes:

  • django.function_name
  • context

Adapting Tests

sed commands used for converting transaction context managers:

  • sed -i '' 's/with sentry_sdk.start_transaction(name="test_transaction"):/with sentry_sdk.traces.start_span(name="custom parent"):/g'
  • sed -i '' 's/with start_transaction(name="test_transaction"):/with sentry_sdk.traces.start_span(name="custom parent"):/g'

sed commands used for converting specific attributes:

  • sed -i '' 's/["request"]["method"]/["attributes"][SPANDATA.HTTP_REQUEST_METHOD]/g'
  • sed -i '' 's/["active_thread_id"]/["attributes"]["thread.id"]/g'
  • sed -i '' 's/CODE_LINENO/CODE_LINE_NUMBER/g'
  • sed -i '' 's/CODE_FILEPATH/CODE_FILE_PATH/g'
  • sed -i '' 's/SPANDATA.DB_OPERATION/SPANDATA.DB_OPERATION_NAME/g'
  • sed -i '' 's/SPANDATA.DB_SYSTEM/SPANDATA.DB_SYSTEM_NAME/g'
  • sed -i '' 's/SPANDATA.DB_NAME/SPANDATA.DB_NAMESPACE/g'

sed commands used for converting event capture:

  • sed -i '' 's/capture_events,/capture_items,/g'
  • sed -i '' 's/capture_envelopes,/capture_items,/g'
  • sed -i '' 's/events = capture_events()/items = capture_items("event", "transaction", "span")/g'
  • sed -i '' 's/envelopes = capture_envelopes()/items = capture_items("event", "transaction", "span")/g'
  • sed -i '' '/event = envelope.get_event()/d'
  • sed -i '' 's/event["spans"]/spans/g'
  • sed -i '' 's/transaction["spans"]/spans/g'
  • sed -i '' 's/(msg_event, error_event, transaction_event) = events/(msg_event, error_event, ) = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/(event,) = events/(event, ) = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/(transaction,) = events/spans = [item.payload for item in items if item.type == "span"]/g'
  • sed -i '' 's/message, transaction = events/message, = (item.payload for item in items if item.type == "span")/g'
  • sed -i '' 's/error_event, transaction_event = events/error_event, = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/transaction = events[0]/spans = [item.payload for item in items if item.type == "span"]/g'

sed commands used for converting op:

  • sed -i '' 's/["op"]/["attributes"]["sentry.op"]/g'
  • sed -i '' 's/.get("op")/["attributes"].get("sentry.op")/g'
  • sed -i '' 's/- op/- sentry.op/g'

sed commands used for converting origin:

  • sed -i '' 's/["contexts"]["trace"]["origin"]/["attributes"]["sentry.origin"]/g'
  • sed -i '' 's/["origin"]/["attributes"]["sentry.origin"]/g'

sed commands used for converting description:

  • sed -i '' 's/description/name/g'

sed commands used for converting data to attributes:

  • sed -i '' 's/["data"]/["attributes"]/g'
  • sed -i '' 's/data = span.get("data", {})/attributes = span.get("attributes", {})/g'
  • sed -i '' 's/in data/in attributes/g'
  • sed -i '' 's/data.get/attributes.get/g'

sed commands for converting trace id:

  • sed -i '' 's/["contexts"]["trace"]["trace_id"]/["trace_id"]/g'

sed commands used for converting timestamps:

  • sed -i '' 's/span.timestamp/span._end_timestamp/g'
  • sed -i '' 's/span.start_timestamp/span._start_timestamp/g'

other test changes:

  • sed -i '' '/assert transaction_event["type"] == "transaction"/d'
  • sed -i '' '/assert len(transactions) == 1/d'

Issues

Closes #6015

Reminders


@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

Codecov Results 📊

282 passed | Total: 282 | Pass Rate: 100% | Execution Time: 43.82s

All tests are passing successfully.

❌ Patch coverage is 0.95%. Project has 14983 uncovered lines.

Files with missing lines (11)
File Patch % Lines
tracing_utils.py 26.23% ⚠️ 568 Missing and 24 partials
__init__.py 4.92% ⚠️ 406 Missing
caching.py 0.00% ⚠️ 143 Missing
asyncpg.py 9.87% ⚠️ 137 Missing
asgi.py 0.00% ⚠️ 121 Missing
templates.py 0.00% ⚠️ 110 Missing
middleware.py 0.00% ⚠️ 94 Missing
sqlalchemy.py 10.00% ⚠️ 90 Missing
views.py 0.00% ⚠️ 66 Missing
signals_handlers.py 0.00% ⚠️ 49 Missing
tasks.py 0.00% ⚠️ 28 Missing

Generated by Codecov Action

Comment thread sentry_sdk/integrations/django/middleware.py Outdated
Comment thread tests/integrations/django/test_db_transactions.py
Comment thread tests/integrations/django/test_db_transactions.py
Comment thread tests/integrations/django/test_basic.py
Comment thread tests/integrations/django/test_cache_module.py Outdated
Comment thread sentry_sdk/tracing_utils.py
Comment thread tests/integrations/django/test_basic.py
Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Span thread.id assertion will crash: UnwrappedItem isn't subscriptable and span payload has no contexts key (tests/integrations/django/asgi/test_asgi.py:231)

In tests/integrations/django/asgi/test_asgi.py (around L231–L235) the span_streaming branch does:

spans = [item for item in items if item.type == "span"]
for span in spans:
    trace_context = span["contexts"]["trace"]
    assert str(data["active"]) == trace_context["attributes"]["thread.id"]

Two problems:

  1. capture_items (tests/conftest.py ~L340) returns UnwrappedItem dataclass instances (fields type, payload) with no __getitem__, so span["contexts"] raises TypeError: 'UnwrappedItem' object is not subscriptable. Every other call site in the repo correctly uses item.payload for item in items if item.type == "span".
  2. Even after switching to span.payload, the unwrapped span payload follows the OTel v2 span schema (trace_id, span_id, name, attributes, ... — see tests/tracing/test_span_batcher.py ~L407–420). There is no contexts.trace envelope around it, so span.payload["contexts"] would raise KeyError.

As long as at least one span is emitted for /sync/thread_ids / /async/thread_ids (which is the point of the test), the assertion crashes before it can validate the streaming thread-id behavior, defeating the purpose of the new test parametrization.

Verification

Inspected tests/conftest.py ~L340 (UnwrappedItem is a plain @dataclass with type/payload, no __getitem__) and tests/conftest.py ~L367–L371 (for span items it stores the per-item dict from item.payload.json["items"] directly into payload, not under a contexts key). Confirmed via tests/tracing/test_span_batcher.py ~L407 that streaming span payloads are flat OTel-style (trace_id, span_id, attributes, ...). All other span-iterating tests in the repo (anthropic, sqlalchemy, fastmcp, openai, tracing) use item.payload for item in items if item.type == "span" and access attributes directly.

Test loop may pass vacuously when no spans are captured (tests/integrations/django/asgi/test_asgi.py:233)

There is no assertion that spans is non-empty before the for span in spans loop, so if the streaming path emits no spans the assertions inside the loop are never executed and the test passes silently — unlike the else branch which has assert len(transactions) == 1.

Verification

In the else branch (lines 248, 263) assert len(profiles) == 1 and assert len(transactions) == 1 guard both loops. The span_streaming=True branch has no equivalent guard, so an empty spans list produces a vacuously-passing test.

test_sql_queries and test_sql_dict_query_params never configure SDK for streaming mode (tests/integrations/django/test_basic.py:700)

Both tests parametrize span_streaming but their sentry_init call only sets _experiments={"record_sql_params": True} — the "trace_lifecycle": "stream" key is never set, so the span_streaming=True variant runs with the default (static) lifecycle and isn't testing streaming at all.

Verification

In test_queryset_repr (line ~553) the sentry_init correctly uses _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}. In test_sql_queries (line ~697) the sentry_init uses only _experiments={"record_sql_params": True} with no trace_lifecycle key regardless of the span_streaming value. Same pattern in test_sql_dict_query_params (~line 750). The span_streaming=True branch uses capture_items("event") instead of capture_events(), but because the SDK is running in static mode, behavior is identical to the False branch and actual streaming code paths are never exercised.

Identified by Warden code-review

Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncpg streaming path sets deprecated db.system instead of db.system.name

In sentry_sdk/integrations/asyncpg.py, _set_db_data and _wrap_connect_addr both use SPANDATA.DB_SYSTEM ("db.system") in the streaming path; they should use SPANDATA.DB_SYSTEM_NAME ("db.system.name") like the sqlalchemy integration does.

Verification
  • asyncpg.py line 266: set_value(SPANDATA.DB_SYSTEM, "postgresql") uses the same deprecated key for both streaming and non-streaming paths.
  • asyncpg.py line 213: SPANDATA.DB_SYSTEM: "postgresql" is placed directly in the streaming span_attributes dict.
  • sqlalchemy.py _set_db_data correctly branches: span.set_attribute(SPANDATA.DB_SYSTEM_NAME, ...) for StreamedSpan vs span.set_data(SPANDATA.DB_SYSTEM, ...) for Span.
  • The PR description explicitly states to use db.system.name instead of db.system in the streaming path.
  • Test test_asyncpg.py line 1401 confirms the wrong behaviour by asserting bind_exec_span["attributes"]["db.system"] == "postgresql" in the streaming case — the tests are aligned to the bug, not the spec.

asyncpg streaming path sets deprecated db.name instead of db.namespace

In sentry_sdk/integrations/asyncpg.py, _set_db_data (line 279) and _wrap_connect_addr (line 216) both use SPANDATA.DB_NAME ("db.name") in the streaming path; they should use SPANDATA.DB_NAMESPACE ("db.namespace") like the sqlalchemy integration does.

Verification
  • asyncpg.py line 279: set_value(SPANDATA.DB_NAME, database) is called with the same deprecated key for both streaming and non-streaming paths via the unified set_value alias.
  • asyncpg.py line 216: SPANDATA.DB_NAME: database is placed directly in the streaming span_attributes dict inside _wrap_connect_addr.
  • sqlalchemy.py _set_db_data correctly uses span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) for StreamedSpan and span.set_data(SPANDATA.DB_NAME, ...) for Span.
  • The PR description explicitly states to use db.namespace instead of db.name in the streaming path.
  • Test test_asyncpg.py line 1406 asserts bind_exec_span["attributes"]["db.name"] == PG_NAME in the streaming path, confirming the tests encode the wrong attribute name.
  • consts.py marks DB_NAME = "db.name" as deprecated with a note to use DB_NAMESPACE instead.

Identified by Warden find-bugs

Comment thread tests/integrations/django/test_cache_module.py
Comment thread tests/integrations/django/test_db_query_data.py
Comment thread tests/integrations/django/asgi/test_asgi.py Outdated
Comment thread tests/integrations/django/test_basic.py
Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncpg streaming connect span sets deprecated db.system/db.name instead of db.system.name/db.namespace

The asyncpg streaming connect span (and _set_db_data) uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — both marked deprecated in favor of DB_SYSTEM_NAME (db.system.name) and DB_NAMESPACE (db.namespace) — while the Django integration correctly uses the new names in the same PR.

Verification

In sentry_sdk/integrations/asyncpg.py the streaming connect path (lines ~213–216) builds span_attributes with keys SPANDATA.DB_SYSTEM and SPANDATA.DB_NAME. The _set_db_data helper (lines ~266, ~279) also uses the same deprecated constants when invoked with a StreamedSpan. By contrast, sentry_sdk/integrations/django/__init__.py's _set_db_data correctly branches on isinstance(span, StreamedSpan) and uses SPANDATA.DB_SYSTEM_NAME / SPANDATA.DB_NAMESPACE for streaming spans. The test_asyncpg.py streaming tests only assert on breadcrumbs (CRUMBS_CONNECT contains db.system and db.name) and never check the span attributes, so this inconsistency is not caught by the test suite.

Identified by Warden find-bugs

Comment thread tests/integrations/django/test_db_query_data.py
Comment thread tests/integrations/django/test_db_query_data.py
@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review May 19, 2026 14:42
@alexander-alderman-webb alexander-alderman-webb requested a review from a team as a code owner May 19, 2026 14:42
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 285de05. Configure here.

Comment thread sentry_sdk/integrations/django/views.py
Comment thread tests/integrations/django/asgi/test_asgi.py Outdated
@alexander-alderman-webb alexander-alderman-webb marked this pull request as draft May 19, 2026 14:48
@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review May 19, 2026 15:10
Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncpg streaming path uses deprecated db.system and db.name attributes instead of db.system.name and db.namespace

In asyncpg.py _set_db_data, set_value is bound to span.set_attribute for StreamedSpan but still uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — the deprecated keys — instead of SPANDATA.DB_SYSTEM_NAME and SPANDATA.DB_NAMESPACE required by the streaming path. The django and sqlalchemy integrations in this same PR correctly use the new keys.

Verification

Checked sentry_sdk/integrations/asyncpg.py lines 264-279 (_set_db_data): set_value = span.set_attribute if isinstance(span, StreamedSpan) else span.set_data, then set_value(SPANDATA.DB_SYSTEM, ...) (line 266) and set_value(SPANDATA.DB_NAME, ...) (line 279) — both using the deprecated key names for both streaming and non-streaming paths. SPANDATA.DB_SYSTEM = 'db.system' (deprecated, per consts.py:511-514) and SPANDATA.DB_SYSTEM_NAME = 'db.system.name' (new, per consts.py:521). Same for DB_NAME vs DB_NAMESPACE. By contrast, sqlalchemy.py _set_db_data (lines ~144-171) and django/__init__.py _set_db_data (lines 776-820) both branch on isinstance(span, StreamedSpan) and use DB_SYSTEM_NAME/DB_NAMESPACE for the streaming path. No grep match for DB_SYSTEM_NAME or DB_NAMESPACE in asyncpg.py confirms neither is used.

Identified by Warden find-bugs

Comment thread sentry_sdk/tracing_utils.py
Comment thread tests/integrations/django/asgi/test_asgi.py
Comment thread tests/integrations/django/test_db_transactions.py
Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record_sql_queries silently drops SQL debug data (db.params, db.executemany) for streaming spans

In tracing_utils.py, the data dict (containing db.params, db.paramstyle, db.executemany, db.cursor) is built then applied to the span via span.set_data() in non-streaming mode, but the streaming path yields the span without setting any of these attributes, silently losing the data even when record_sql_params is enabled.

Verification

Checked sentry_sdk/tracing_utils.py lines 141-171. The data dict is unconditionally populated (conditioned on params_list, paramstyle, executemany, record_cursor_repr). In the else branch (non-streaming), for k, v in data.items(): span.set_data(k, v) correctly applies them. In the if has_span_streaming_enabled(...) branch (streaming), the with sentry_sdk.traces.start_span(...) block immediately yields the span with no equivalent attribute-setting loop. Callers like Django's execute() and asyncpg's _record() pass params_list and executemany=True/record_cursor_repr=True expressly to have them recorded, but in streaming mode none of this data reaches the span.

Identified by Warden find-bugs

Copy link
Copy Markdown
Contributor

@sentrivana sentrivana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, including the data that we're dropping in span first (like template context) -- we can re-add if there's demand for it.

Comment thread sentry_sdk/integrations/wsgi.py Outdated
Comment thread sentry_sdk/integrations/django/__init__.py
@alexander-alderman-webb alexander-alderman-webb merged commit 9084c9a into master May 20, 2026
155 of 157 checks passed
@alexander-alderman-webb alexander-alderman-webb deleted the webb/django/span-first branch May 20, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate django to span first

2 participants